package cn.baage.campus.service.impl;

import cn.baage.campus.config.properties.BaageProperties;
import cn.baage.campus.config.security.BaseUserDetail;
import cn.baage.campus.config.security.auth.WxAuthenticationToken;
import cn.baage.campus.constant.BaageConstant;
import cn.baage.campus.constant.CacheConstant;
import cn.baage.campus.constant.FilePathConstant;
import cn.baage.campus.enums.ExceptionType;
import cn.baage.campus.exception.BizException;
import cn.baage.campus.model.dto.LoginUser;
import cn.baage.campus.model.po.User;
import cn.baage.campus.model.po.UserRole;
import cn.baage.campus.model.vo.ForgetReqVO;
import cn.baage.campus.service.AuthService;
import cn.baage.campus.service.RedisService;
import cn.baage.campus.service.UserRoleService;
import cn.baage.campus.service.UserService;
import cn.baage.campus.utils.NicknameUtils;
import cn.baage.campus.utils.SecurityUtils;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.*;


/**
 * @Description: 认证Service实现
 * @Author: 八阿哥
 * @url: baage.cn
 * @Version: 1.0
 **/
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Slf4j
public class AuthServiceImpl implements AuthService {

    private final AuthenticationManager authenticationManager;
    private final RedisService<Object> redisService;
    private final UserService userService;
    private final PasswordEncoder passwordEncoder;
    private final UserRoleService userRoleService;
    private final BaageProperties baageProperties;


    @Override
    public Map<String, String> loginByUserName(String username, String password) {
        // 1. 用户认证
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username, password);
        // 认证并获取认证后的结果
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (Objects.isNull(authenticate)) {
            log.debug("用户 {} 登录失败", username);
            throw new RuntimeException("登陆失败");
        }
        // 2. 生成Token，将 userId:token 保存到缓存
        Map<String, String> authResult = successAuthentication(authenticate);
        log.info("用户 {} 登录成功", username);
        return authResult;
    }


    public Map<String, String> loginByWx(String openid) {
        WxAuthenticationToken wxAuthenticationToken = new WxAuthenticationToken(openid);
        Authentication authenticate = authenticationManager.authenticate(wxAuthenticationToken);
        if (Objects.isNull(authenticate)) {
            throw new UsernameNotFoundException("当前微信未绑定用户信息！");
        }
        Map<String, String> authResult = successAuthentication(authenticate);
        log.info("用户登录成功，openid {}", openid);
        return authResult;
    }


    @Override
    public void logout() {
        BaseUserDetail loginUserInfo = SecurityUtils.getLoginUserDetail();
        LoginUser loginUser = loginUserInfo.getLoginUser();
        String cacheKey = CacheConstant.AUTH_KEY + loginUser.getId() + ":" + loginUserInfo.getCode();
        log.info("用户 {} 退出登录", loginUser.getUsername());
        redisService.delete(cacheKey);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void register(User user) {
        User byUserName = userService.getByUserName(user.getUsername());
        if (byUserName != null) {
            throw new BizException(ExceptionType.USERNAME_UK_ERROR);
        }
        User byEmail = userService.getByEmail(user.getEmail());
        if (byEmail != null) {
            throw new BizException(ExceptionType.EMAIL_UK_ERROR);
        }
        String id = IdUtil.simpleUUID();
        Date currentTime = new Date();
        user.setId(id);
        user.setNickName(NicknameUtils.generateNickname());
        Random random = new Random();
        String randomAvatar = String.format("%s/default_%d.jpg", FilePathConstant.AVATAR_PATH, random.nextInt(12) + 1);
        user.setAvatarUrl(randomAvatar);
        user.setCreateTime(currentTime);
        user.setUpdateTime(currentTime);
        user.setStatus(1);
        user.setDelFlag(false);
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        UserRole userRole = new UserRole();
        userRole.setUserId(id);
        userRole.setRoleId(baageProperties.getDefaultRole());
        userRoleService.save(userRole);
        userService.save(user);
    }


    /**
     * 找回密码
     *
     * @param forgetReqVO 新信息
     */
    @Override
    public void forget(ForgetReqVO forgetReqVO) {
        LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
        userLambdaQueryWrapper.eq(User::getEmail, forgetReqVO.getEmail());
        User user = userService.getOne(userLambdaQueryWrapper);
        if (user == null) {
            throw new BizException(ExceptionType.USER_NOT_FOUND_ERROR);
        }
        user.setPassword(passwordEncoder.encode(forgetReqVO.getPassword()));
        userService.updateUserInfo(user);
    }


    /**
     * 淘汰token
     *
     * @param userId 用户ID
     */
    private void cullingOldToken(String userId) {
        String cacheKey = CacheConstant.AUTH_KEY + userId + ":*";
        Set<String> keys = redisService.getKeys(cacheKey);
        log.debug("获取到 keys {}", JSON.toJSONString(keys));
        String delKey;
        if (keys != null && keys.size() > baageProperties.getLoginLimit() + 1) {
            keys.remove(userId);
            Iterator<String> keyIterator = keys.iterator();
            delKey = keyIterator.next();
            Long recentExpire = redisService.getExpire(delKey);
            while (keyIterator.hasNext()) {
                String nextKey = keyIterator.next();
                Long nextExpire = redisService.getExpire(nextKey);
                if (nextExpire < recentExpire) {
                    delKey = nextKey;
                }
            }
            redisService.delete(delKey);
        }
    }


    /**
     * 获取登录设备信息
     *
     * @return 登录设备信息
     */
    private Map<String, Object> getAgentInfo() {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert servletRequestAttributes != null;
        HttpServletRequest request = servletRequestAttributes.getRequest();
        String header = request.getHeader("User-Agent");
        UserAgent agent = UserAgentUtil.parse(header);
        if (agent == null) {
            return new HashMap<>();
        }
        log.info("登录信息：{}", JSON.toJSONString(agent));
        Map<String, Object> agentMap = new HashMap<>();
        agentMap.put("isMobile", agent.isMobile());
        if (agent.getOs().getName() != null){
            agentMap.put("os", agent.getOs().getName());
        }
        if (agent.getBrowser().getName() != null) {
            agentMap.put("browser", agent.getBrowser().getName());
        }
        return agentMap;
    }


    /**
     * 认证成功操作
     *
     * @param authentication 认证信息
     * @return 相应
     */
    private Map<String, String> successAuthentication(Authentication authentication) {
        String code = IdUtil.simpleUUID();
        BaseUserDetail baseUserDetail = (BaseUserDetail) authentication.getPrincipal();
        LoginUser loginUser =  baseUserDetail.getLoginUser();
        String cacheInfoKey = CacheConstant.AUTH_KEY + loginUser.getId() + ":info" + loginUser.getId();
        String cacheTokenKey = CacheConstant.AUTH_KEY + loginUser.getId() + ":" + code;
        redisService.set(cacheInfoKey, loginUser, BaageConstant.TOKEN_EXPIRATION);
        redisService.set(cacheTokenKey, JSON.toJSONString(getAgentInfo()) , BaageConstant.TOKEN_EXPIRATION);
        cullingOldToken(loginUser.getId());
        return new HashMap<String, String>(){{
            put("token", code + BaageConstant.TOKEN_SPLIT + loginUser.getId());
            put("username", loginUser.getUsername());
        }};
    }

}
